#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2019/6/18 17:12
# @Author  : CHEN Wang
# @Site    : 
# @File    : common_wrappers.py
# @Software: PyCharm

import sys
import time
import datetime
import pandas as pd
import prettytable
import functools
from functools import wraps
from quant_researcher.quant.datasource_fetch.common_data_api.t_trade_date import get_trade_date_by_mkt, HS_MARKET
from quant_researcher.quant.project_tool.logger.my_logger import LOG
from quant_researcher.quant.project_tool import time_tool, common_var
from quant_researcher.quant.project_tool import slim
from quant_researcher.quant.project_tool import hammer
from quant_researcher.quant.project_tool import parser_distributor


def get_interval_from_env(func):
    """
    从环境变量中获取脚本运行的日期区间，并用 date1（开始），date2（截止）作为关键字给函数
        - 为什么是 date1 和 date2 来表示，历史遗留。最开始想着从日期1 运行到 日期2 挺顺口
        - 注：以英文逗号分隔；没有传 None（注意大小写）
        - 示例值：None,2020-01-01 或 2020-03-01,None 或 2020-01-01,2020-02-22
    :param func:
    :return:
    """
    @wraps(func)
    def inner(*args, **kwargs):
        interval_str = slim.get_task_properties_from_env()[slim.K_INIT_INTERVAL]
        if interval_str is not None:
            tmp = interval_str.split(',')
            if len(tmp) != 2:
                err_msg = '指定初始化区间格式错误！应该：以英文逗号分隔的两个字符串；' \
                          f'如果没有开始或结束，传入“None”。你传的是：{interval_str}'
                raise ValueError(err_msg)
            date1, date2 = tmp
            if date1 == 'None':
                date1 = None
            if date2 == 'None':
                date2 = None
            LOG.warning(f'从 env 取到了区间信息：{interval_str}。'
                        f'解析为：date1={date1}，date2={date2}')
            kwargs.update({'date1': date1, 'date2': date2})
        ans = func(*args, **kwargs)
        return ans
    return inner


def do_filtering_if_debugging(**kwds):
    """
    自动读取配置判断是不是需要缩减计算量，根据传入参数确定如何缩减
        - 在配置文件中寻找 debugging.debugging_mode 值
        - 该值的 bool 运算结果为 真，则缩减；否则，跳过
    :param kwds:
        - input_class，str，需要缩减时，针对的是哪种类型的对象，不同类型方式不同
        - a_condition，str，需要缩减时，针对 SQL 追加的条件
        - test_col，str，需要缩减时，针对 pd.DataFrame 进行过滤的列
        - test_code，str，需要缩减时，针对 pd.DataFrame 某列过滤的值
        - sample_size，int，需要缩减时，随机样的个数
    :return:
    """
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            pass
        return inner
    return wrapper


def automatic_initialization_checker(func_2_initialize):
    """
    自动判断现在是否初始化，如果是则调用提前

    :param func func_2_initialize: 如果是初始化，则被调用的函数
    :return: wrapper，带入参的 wrapper
    """
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            func_2_run = func_2_initialize if slim.if_initialize() else func
            return func_2_run(*args, **kwargs)
        return inner
    return wrapper


def automatic_date_formatter_wrapper(table_specifics):
    """
    用来解决表查询时，不同的日期格式，class的问题，
        根据要求传入参数，则能将结果自动转换成程序里面通用的fmt和class
    :param dict table_specifics: 见示例，全局搜搜，看看用法
    :return: func, the function being decorated
    """
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            LOG.warning('目前还没针对 多列的 自动转换进行 测试 哦！')
            LOG.warning('目前还没针对 无日期约束的 自动转换进行 编码 哦！')
            # print(locals())
            date_related_filter = kwargs.pop('date_related_filter', None)
            cols_need_to_modify_after_querying = []
            my_date_fmt = common_var.MY_DATE_FMT
            my_date_class = common_var.MY_DATE_CLASS
            if date_related_filter is not None:
                date_specifics = []

                def deal_a_bond(x):
                    if x is not None:
                        if (my_date_class == 'int' and not isinstance(x, int)) \
                                or (my_date_class == 'str' and not isinstance(x, str)):
                            raise RuntimeError(f'配置的日期类型 {my_date_class} 和实际传入的 {x.__class__} 不符')
                        if my_date_fmt != table_specifics[col_name]['fmt']:
                            if my_date_class == 'int':
                                x = str(x)
                            x = time_tool.format_date_str(x,
                                                          target_fmt=table_specifics[col_name]['fmt'])  # 这个函数存在的太合理了！！！
                        if my_date_class != table_specifics[col_name]['class']:  # 只有当两处的字段类型不一样时才处理
                            if table_specifics[col_name]['class'] == 'normal_date':  # 如果数据库表的这列格式为date，那么只需要用字符串传进去就OK
                                if my_date_class != 'str':  # 只有当我们程序中使用的不是str时，才进行转类型
                                    x = str(x)
                            else:  # 源表类型不和我们使用的一样，且源表的类型不是date
                                if table_specifics[col_name]['class'] == 'int':  # 算了，还是不直接用 方法 去进行配置
                                    x = int(x)
                                elif table_specifics[col_name]['class'] == 'float':
                                    x = float(x)
                                else:
                                    raise NotImplementedError(table_specifics[col_name]['class'])
                    return x

                for col_name, col_from, col_to in date_related_filter:
                    cols_need_to_modify_after_querying.append(col_name)
                    if table_specifics.__contains__(col_name):
                        if not my_date_fmt == table_specifics[col_name]['fmt']:
                            col_from = deal_a_bond(col_from)
                            col_to = deal_a_bond(col_to)
                    if col_from is not None:
                        if table_specifics[col_name]['fmt'] == 'int':
                            date_specifics.append(f'{col_from}<={col_name}')
                        else:
                            date_specifics.append(f'\'{col_from}\'<={col_name}')
                    if col_to is not None:
                        if table_specifics[col_name]['fmt'] == 'int':
                            date_specifics.append(f'{col_name}<={col_to}')
                        else:
                            date_specifics.append(f'{col_name}<=\'{col_to}\'')
                date_specifics = [f'({x})' for x in date_specifics]
                date_specifics = ' and '.join(date_specifics)
                if date_specifics == '':
                    date_specifics = None
                kwargs.update({'date_related_filter': date_specifics})
            ans = func(*args, **kwargs)
            try:
                for a_col in cols_need_to_modify_after_querying:
                    if my_date_fmt != table_specifics[a_col]['fmt']:
                        ans.loc[:, a_col] = ans[a_col].apply(
                            time_tool.format_date_str,
                            **{'target_fmt': my_date_fmt})
                    if my_date_class != table_specifics[a_col]['class']:
                        if my_date_class == 'int':
                            ans.loc[:, a_col] = ans[a_col].apply(int)
                        elif my_date_class == 'str':
                            ans.loc[:, a_col] = ans[a_col].apply(str)
                        else:
                            raise NotImplementedError(my_date_class)
            except Exception as err:
                print(ans)
                print('--引起自动列转换失败的数据--')
                raise err
            return ans
        return inner
    return wrapper


def rename_date_col_to_tj(a_col):
    """
    自动把返回的 pd.DataFrame 中的指定列重命名成 tj
        为了统一命名而成立
    :param str a_col: 数据中哪一列代表日期
    :return: wrapper
    """
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            df = func(*args, **kwargs)
            df_cols = df.columns.values
            if a_col not in df_cols:
                hammer.slim(df, name='尝试自动更改日期列名的DF', a_f=1)
                raise RuntimeError(f'未在DF的列({df_cols})中找到{a_col}')
            df.rename(columns={a_col: 'tj'}, inplace=True)
            # hammer.slim(df, name='更改日期列名为 tj 后的DF', a_f=1, show_index=1)
            return df
        return inner
    return wrapper


def debug_wrapper(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """
        the inner function

        :param args:
        :param kwargs:
            debug_mode，bool，是不是现在正在调试这个函数，默认：False
            正在尝试这种写法，好像只是适合关键变量比较少的函数？？？
        :return: func, the function being decorated
        """
        debug_mode = kwargs.pop('debug_mode', False)
        debug_msg = kwargs.pop('debug_msg', '-')
        p_table = None
        if debug_mode:
            print('-' * 88)
            print('👺：现在正在调试这个函数')
            p_table = prettytable.PrettyTable(['name', 'value'])
            p_table.add_row(['函数', func.__name__])
            p_table.add_row(['Info', debug_msg])

        def add_row_to_table(row):
            if p_table:
                p_table.add_row(row)

        kwargs.update({'test_func': add_row_to_table})
        ans = func(*args, **kwargs)

        if debug_mode:
            print(p_table)
            print('-' * 88)
        return ans

    return wrapper


def run_from_cmd_wrapper(name):
    """
    如果从 cmd 运行该函数，自动去寻找之前定义好的参数解析对象

    :param str name: 参数解析对象名
    :return: wrapper
    """
    def wrapper(func):
        @wraps(func)
        def inner():
            parser = parser_distributor.get_parser(name)
            args_parsed = parser.parse_args(sys.argv[1:])
            result = func(args_parsed)
            return result
        return inner
    return wrapper


def count_time(func):
    """
    函数运行时间统计 wrapper

    :param func: 函数
    :return: wrapper
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = datetime.datetime.now()  # 程序开始时间
        LOG.warning('函数：%s，开始时间：%s', func.__name__, start_time)
        result = func(*args, **kwargs)
        over_time = datetime.datetime.now()  # 程序结束时间
        total_sec = (over_time - start_time).total_seconds()
        total_min = total_sec / 60.0
        LOG.warning('程序：%s，共计:%.2f秒≈%.2f分\n开始时间：%s\n结束时间：%s' % (
            func.__name__, total_sec, total_min, start_time, over_time
        ))
        return result
    return wrapper


def count_time_print(func):
    """同上，将来可能删除"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = datetime.datetime.now()  # 程序开始时间
        result = func(*args, **kwargs)
        over_time = datetime.datetime.now()  # 程序结束时间
        total_sec = (over_time - start_time).total_seconds()
        total_min = total_sec / 60.0
        print('程序：%s，共计:%.2f秒≈%.2f分\n开始时间：%s\n结束时间：%s' % (
            func.__name__, total_sec, total_min, start_time, over_time
        ))
        return result
    return wrapper


def func_run_log(func):
    def wrapper(*args, **kw):
        print(f'calling {func.__name__}')
        out = func(*args, **kw)
        print('finished')
        return out
    return wrapper


def run_during_intervals(intervals, **kwds):
    """
    只在一天中的特定时间段运行代码如：09:00:00-11:00:00，分别是开始和结束的时分秒指定
    :param intervals: 单个或多个运行区间
    :param kwds:
        - fallback，函數，如果不運行，那麼被運行的函數，默認：None
    :return:
        - 如运行，就是函数的结果
        - 否则：None（無 fallback函數）或 fallback函數 的結果
    """
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            right_now = datetime.datetime.now()
            right_now = time_tool.format_date_str(right_now, target_fmt=common_var.PY_TIME_FMT_WITH_N_DASH)
            right_now = right_now[-8:]

            # DEBUG
            # right_now = '11:30:30'

            intervals_msg = ' 或 '.join(intervals)
            intervals_msg = f'运行区间：{intervals_msg}。现在时间：{right_now}。'
            a_go = False
            for a in intervals:
                start, end = a.split('-')
                if start <= right_now <= end:
                    a_go = True
            if a_go:
                LOG.info(f'{intervals_msg}满足运行条件，开始 ...')
                return func(*args, **kwargs)
            else:
                LOG.info(f'{intervals_msg}不满足运行条件，嘗試尋找fallback函數。')
                fallback_func = kwds.get('fallback', None)
                if fallback_func is None:
                    LOG.info(f'fallback函數為None，直接 return。')
                    return
                else:
                    LOG.info(f'fallback函數：{fallback_func.__name__}，開始 ...')
                    return fallback_func(*args, **kwargs)
        return inner
    return wrapper


def assert_date_index(f):
    @wraps(f)
    def wrapper(series, *args, **kw):
        assert isinstance(series.index,pd.DatetimeIndex), f'请确认index为日期类型, 现在为{series.index.dtype}'
        return f(series, *args, **kw)
    return wrapper


def assert_series(f):
    @wraps(f)
    def wrapper(series, *args, **kw):
        assert isinstance(series,pd.core.series.Series), f'请确认为Series, 现在为{type(series)}'
        return f(series, *args, **kw)
    return wrapper


def execute_if_it_is_a_trade_date(func):
    @wraps(func)
    def inner(*args, **kwargs):
        today = time_tool.get_today()
        df = get_trade_date_by_mkt(target_date=today)
        if df.shape[0] == 0:
            LOG.warning(f'今天（{today}），'
                        f'在市场（代码：{HS_MARKET}）看起来好像不是交易日，'
                        f'就不执行这个函数了吧！')
            return
        else:
            return func(*args, **kwargs)
    return inner


@execute_if_it_is_a_trade_date
def tt():
    print('hello')


def exe_if_trading_according_2_today_in_kwargs(func):
    @wraps(func)
    def inner(*args, **kwargs):
        today = kwargs.get('today', None)
        if today is None:
            today = time_tool.get_today()
        df = get_trade_date_by_mkt(target_date=today)
        if df.shape[0] == 0:
            LOG.warning(f'今天（{today}），'
                        f'在市场（代码：{HS_MARKET}）看起来好像不是交易日，'
                        f'就不执行这个函数了吧！')
            return
        else:
            return func(*args, **kwargs)
    return inner


def deco_retry(retry: int = 5, retry_sleep: int = 3):
    """
    :param retry: 重试次数
    :param retry_sleep: 重试之间间隔时间（单位为秒）
    :return:
    """
    def deco_func(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            _retry = 5 if callable(retry) else retry
            _result = None
            for _i in range(1, _retry + 1):
                try:
                    print(f'进行第{_i}次尝试')
                    _result = func(*args, **kwargs)
                    break
                except Exception as e:
                    LOG.warning(f"{func.__name__}: {_i} :{e}")
                    if _i == _retry:
                        raise
                time.sleep(retry_sleep)
            return _result

        return wrapper

    return deco_func(retry) if callable(retry) else deco_func


if __name__ == '__main__':
    # 测试execute_if_it_is_a_trade_date
    tt()